package fido;


import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;

class StringComparator
        implements
        Comparator<String>,
        Serializable {

    private static final long serialVersionUID = 5603928894805771653L;

    public static final StringComparator DEFAULT = new StringComparator();

    /**
     * Compares two strings.
     */
    public int compare(
            final String pString1,
            final String pString2) {

        if (pString1 == null) {
            return Integer.MIN_VALUE;
        }

        if (pString2 == null) {
            return Integer.MAX_VALUE;
        }

        if (pString1.equals(pString2)) {
            return 0;
        }

        if (pString1.equals(pString2)) {
            return 0;
        }

        final int aS1Length = pString1.length();
        final int aS2Length = pString2.length();
        final int aMinLength = Math.min(aS1Length, aS2Length);
        for (int i = 0; i < aMinLength; i++) {
            char aC1 = pString1.charAt(i);
            char aC2 = pString2.charAt(i);

            if (aC1 != aC2) {
                return aC1 - aC2;
            }
        }

        return aS1Length - aS2Length;
    }

}

class ClassComparator
        implements
        Comparator<Class<?>>,
        Serializable {

    private static final long serialVersionUID = -7120534646583185358L;

    public static final ClassComparator DEFAULT = new ClassComparator();

    /**
     * Compares two classes.
     */
    public int compare(
            final Class<?> pClass1,
            final Class<?> pClass2) {

        if (pClass1 == null) {
            return Integer.MIN_VALUE;
        }

        if (pClass2 == null) {
            return Integer.MAX_VALUE;
        }

        if (pClass1.equals(pClass2)) {
            return 0;
        }

        final String aC1Name = pClass1.getCanonicalName();
        final String aC2Name = pClass2.getCanonicalName();
        return StringComparator.DEFAULT.compare(aC1Name, aC2Name);
    }

}

public class ClassDiscovery {

    private static String getClassNameAfterPackageAsPath(
            final String pFileName,
            final String pPkgAsPath) {
        final String cName;
        cName = pFileName.substring(0, pFileName.length() - 6).replace('/', '.').replace('\\', '.');
        return cName.substring(pPkgAsPath.length());
    }

    private static String getClassNameOfPackageAsPath(
            final String pFileName,
            final String pPkgAsPath) {

        final boolean aIsClass = pFileName.endsWith(".class");
        if (!aIsClass) {
            return null;
        }

        final boolean aIsBelongToPackage = pFileName.startsWith(pPkgAsPath);
        if (!aIsBelongToPackage) {
            return null;
        }

        return ClassDiscovery.getClassNameAfterPackageAsPath(pFileName, pPkgAsPath);
    }

    private static File getPackageFile(
            final String pPkgName,
            final File pPkgPath) {

        final String aPkgFileName = pPkgPath.getAbsoluteFile().toString() + '/' + pPkgName.replace('.', '/');
        final File aPkgFile = new File(aPkgFileName);

        final boolean aIsExist = aPkgFile.exists();
        final boolean aIsDirectory = aPkgFile.isDirectory();
        final boolean aIsExist_asDirectory = aIsExist && aIsDirectory;
        if (!aIsExist_asDirectory) {
            return null;
        }

        return aPkgFile;
    }

    private static boolean IsJarFile(final File pFile) {
        return pFile.toString().endsWith(".jar");
    }

    private static ArrayList<String> discoverClassNamesFromJarFile(final PkgInfo pPkgInfo) {

        final ArrayList<String> aClassNames = new ArrayList<String>();
        try {
            final JarFile jarFile = new JarFile(pPkgInfo.PkgPath);
            final Enumeration<JarEntry> jarEntries = jarFile.entries();

            while (jarEntries.hasMoreElements()) {
                final JarEntry aJE = jarEntries.nextElement();
                final String aJEName = aJE.getName();

                final String aSimpleName = getClassNameOfPackageAsPath(aJEName, pPkgInfo.PkgAsPath);
                if (aSimpleName == null) {
                    continue;
                }

                final String aClassName = pPkgInfo.PkgName + '.' + aSimpleName;
                aClassNames.add(aClassName);
            }

            jarFile.close();
        } catch (IOException ignored) {
        }

        return aClassNames;
    }

    private static void discoverClassNamesFromDirectory(
            final String pAbsolutePackagePath,
            final String pPackageName,
            final File pPackageFolder,
            final List<String> pClassNames) {
        final File[] aFiles = pPackageFolder.listFiles();
        for (File aFile : aFiles) {
            if (aFile.isDirectory()) {
                discoverClassNamesFromDirectory(pAbsolutePackagePath, pPackageName, aFile, pClassNames);
                continue;
            }

            final String aFileName = aFile.getAbsolutePath().substring(pAbsolutePackagePath.length() + 1);
            final boolean aIsClassFile = aFileName.endsWith(".class");
            if (!aIsClassFile) {
                continue;
            }

            final String aSimpleName = aFileName.substring(0, aFileName.length() - 6).replace('/', '.').replace('\\', '.');
            final String aClassName = pPackageName + '.' + aSimpleName;
            pClassNames.add(aClassName);
        }
    }

    private static List<String> discoverClassNamesFromDirectory(PkgInfo pPkgInfo) {

        final List<String> aClassNames = new ArrayList<String>();
        final File aPkgFile = ClassDiscovery.getPackageFile(pPkgInfo.PkgName, pPkgInfo.PkgPath);
        if (aPkgFile == null) {
            return aClassNames;
        }

        discoverClassNamesFromDirectory(aPkgFile.getAbsolutePath(), pPkgInfo.PkgName, aPkgFile, aClassNames);
        return aClassNames;
    }

    public static class PkgInfo {
        PkgInfo(
                final File pPkgPath,
                final String pPkgName,
                final String pPkgAsPath) {

            this.PkgPath = pPkgPath;
            this.PkgName = pPkgName;
            this.PkgAsPath = pPkgAsPath;
        }

        final File PkgPath;
        final String PkgName;
        final String PkgAsPath;
    }

    public static PkgInfo GetPackageInfoOf(Class<?> pClass) {
        File aPkgPath = null;
        String aPkgName = null;
        String aPkgAsPath = null;

        try {
            aPkgPath = new File(pClass.getProtectionDomain().getCodeSource().getLocation().toURI());
            aPkgName = pClass.getPackage().getName();
            aPkgAsPath = aPkgName.replace('.', '/') + '/';
        } catch (Exception e) {

        }

        if (aPkgPath == null) {
            return null;
        }

        return new PkgInfo(aPkgPath, aPkgName, aPkgAsPath);
    }

    public static List<String> DiscoverClassNames_inPackage(final PkgInfo pPkgInfo) {

        if (pPkgInfo == null) {
            return null;
        }

        List<String> aClassNames = new ArrayList<String>();
        if (pPkgInfo.PkgPath.isDirectory()) {

            aClassNames = ClassDiscovery.discoverClassNamesFromDirectory(pPkgInfo);

        } else if (pPkgInfo.PkgPath.isFile()) {
            boolean aIsJarFile = ClassDiscovery.IsJarFile(pPkgInfo.PkgPath);
            if (!aIsJarFile) {
                return null;
            }

            aClassNames = ClassDiscovery.discoverClassNamesFromJarFile(pPkgInfo);
        }

        return aClassNames;
    }

    /**
     * Returns an array of class in the same package as the the SeedClass
     *
     * @param pSeedClass   - Seed Class
     * @param pFilterName  - Regular expression to match the desired classes' name (null if no filtering needed)
     * @param pFilterClass - The super class of the desired classes (null if no filtering needed)
     * @return - The array of matched classes, null if there is a problem.
     * (adapted from http://www.javaworld.com/javaworld/javatips/jw-javatip113.html)
     */
    @SuppressWarnings("unchecked")
    public static <T> Class<? extends T>[] DiscoverClasses(
            final Class<?> pSeedClass,
            final String pFilterName,
            final Class<T> pFilterClass) {

        final Pattern aClsNamePattern = (pFilterName == null) ? null : Pattern.compile(pFilterName);

        PkgInfo aPkgInfo = null;

        try {
            aPkgInfo = GetPackageInfoOf(pSeedClass);
        } catch (Exception e) {
        }

        if (aPkgInfo == null) {
            return null;
        }

        final List<String> aClassNames = DiscoverClassNames_inPackage(aPkgInfo);

        if (aClassNames == null) {
            return null;
        }

        if (aClassNames.size() == 0) {
            return null;
        }

        final ArrayList<Class<?>> aClasses = new ArrayList<Class<?>>();
        for (final String aClassName : aClassNames) {

            if ((aClsNamePattern != null) && !aClsNamePattern.matcher(aClassName).matches()) {
                continue;
            }

            // Get the class and filter it
            Class<?> aClass;
            try {
                aClass = Class.forName(aClassName);
            } catch (ClassNotFoundException e) {
                continue;
            } catch (NoClassDefFoundError e) {
                continue;
            }

            if ((pFilterClass != null) && !pFilterClass.isAssignableFrom(aClass)) {
                continue;
            }

            if (pFilterClass != null) {
                if (!pFilterClass.isAssignableFrom(aClass)) {
                    continue;
                }

                aClasses.add(aClass.asSubclass(pFilterClass));
            } else {
                aClasses.add(aClass);
            }
        }

        Collections.sort(aClasses, ClassComparator.DEFAULT);
        Class<? extends T>[] aClassesArray;
        aClassesArray = aClasses.toArray((Class<? extends T>[]) (new Class[aClasses.size()]));

        return aClassesArray;
    }

}
